﻿//-----------------------------------------------------------------------------------
// <copyright file="AppDomainTestMethodInvoker.cs" company="P.O.S Informatique">
//     Copyright (c) P.O.S Informatique. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------------------
namespace PosInformatique.DevelopmentTools.VisualStudio.TestExtensions
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using PosInformatique.Framework;

    /// <summary>
    /// Implementation of <see cref="ITestMethodInvoker"/> which execute the unit test
    /// in a <see cref="AppDomain"/>.
    /// </summary>
    internal sealed class AppDomainTestMethodInvoker : ITestMethodInvoker
    {
        /// <summary>
        /// The MSTest test method invoker context.
        /// </summary>
        private readonly TestMethodInvokerContext context;

        /// <summary>
        /// Initializes a new instance of the <see cref="AppDomainTestMethodInvoker"/>
        /// class with the specified <see cref="TestMethodInvokerContext"/>.
        /// </summary>
        /// <param name="context">MSTest test method invoker context.</param>
        public AppDomainTestMethodInvoker(TestMethodInvokerContext context)
        {
            ContractHelper.IsNotNull(context);

            this.context = context;
        }

        /// <inheritdoc />
        public TestMethodInvokerResult Invoke(params object[] parameters)
        {
            TestMethodInvokerResult result;
            result = new TestMethodInvokerResult();

            // Create UnitTestMethodInvokerInfo for the UnitTestMethodInvoker
            UnitTestMethodInvokerInfo info;
            info = new UnitTestMethodInvokerInfo(this.context.TestMethodInfo);

            // Create the AppDomain where will be invoked the unit test
            string friendlyName;
            friendlyName = string.Format("{0}.{1}()", this.context.TestMethodInfo.ReflectedType.FullName, this.context.TestMethodInfo.Name);

            AppDomain appDomain;
            appDomain = AppDomain.CreateDomain(friendlyName, null, AppDomain.CurrentDomain.SetupInformation);

            try
            {
                UnitTestMethodInvokerResult invokerResult;

                // Create the remote invoker
                RemoteUnitTestMethodInvoker remoteInvoker;
                remoteInvoker = appDomain.CreateInstanceAndUnwrap<RemoteUnitTestMethodInvoker>(info);

                // Call the remote invoker and set the result into the TestMethodInvokerResult
                invokerResult = remoteInvoker.Invoke();
                result.Exception = invokerResult.Exception;
            }
            finally
            {
                AppDomain.Unload(appDomain);
            }

            return result;
        }

        /// <summary>
        /// Internal class use to invoke the <see cref="UnitTestMethodInvoker"/>
        /// in other <see cref="AppDomain"/>.
        /// </summary>
        [Serializable]
        private sealed class RemoteUnitTestMethodInvoker : MarshalByRefObject
        {
            /// <summary>
            /// Info of unit test method to invoke.
            /// </summary>
            private readonly UnitTestMethodInvokerInfo info;

            /// <summary>
            /// Initializes a new instance of the <see cref="RemoteUnitTestMethodInvoker"/> class.
            /// </summary>
            /// <param name="info">Info of unit test method to invoke.</param>
            public RemoteUnitTestMethodInvoker(UnitTestMethodInvokerInfo info)
            {
                ContractHelper.IsNotNull(info);

                this.info = info;
            }

            /// <summary>
            /// Invoke the unit test in the current <see cref="AppDomain"/>.
            /// </summary>
            /// <returns>The result of the unit test method.</returns>
            public UnitTestMethodInvokerResult Invoke()
            {
                using (UnitTestMethodInvoker methodInvoker = new UnitTestMethodInvoker(this.info))
                {
                    return methodInvoker.Invoke();
                }
            }
        }
    }
}
